package it.sephiroth.android.library.easing;

import android.os.Handler;
import android.os.SystemClock;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Helper class to manage the Easing process.<br />
 * Usage:<br />
 * <pre>
 * // Create the easing manager instance 
 * EasingManager manager = new EasingManager( this );
 * // start a Linear easing animation using the easeOut method, from 0.0 to 1.0 
 * // and the duration of 200 ms  
 * manager.start( Linear.class, EaseType.EaseOut, 0.0, 1.0, 200 );
 * </pre>
 * 
 * @author alessandro
 *
 */
public final class EasingManager {

	/**
	 * Implement this callback in order to get updates from
	 * the running easing
	 */
	public interface EasingCallback {
		void onEasingValueChanged(double value, double oldValue);
		void onEasingStarted(double value);
		void onEasingFinished(double value);
	}

	/**
	 * Note that easeNone is valid only used
	 * with easing Linear
	 */
	public enum EaseType {
		EaseIn, EaseOut, EaseInOut, EaseNone
	}
	
	static final int FPS = 60;
	static final int FRAME_TIME = 1000 / FPS;
	static final Handler mHandler = new Handler();

	Easing mEasing;
	Method mMethod;
	boolean mRunning;
	long mBase;
	int mDuration;
	double mStartValue;
	double mEndValue;
	double mValue;
	boolean mInverted;
	EasingCallback mEasingCallback;
	String mToken;
	Ticker mTicker;

	public EasingManager( EasingCallback callback ) {
		mEasingCallback = callback;
		mToken = String.valueOf( System.currentTimeMillis() );
	}

	public void start( Class<? extends Easing> clazz, EaseType type, double fromValue, double endValue, int durationMillis ) {
		start( clazz, type, fromValue, endValue, durationMillis, 0 );
	}
	
	/**
	 * Start the easing with a delay
	 * @param clazz	the Easing class to be used for the interpolation
	 * @param type	the Easing Type
	 * @param fromValue the start value of the easing
	 * @param endValue	the end value of the easing
	 * @param durationMillis	the duration in ms of the easing
	 * @param delayMillis	the delay
	 */
	public void start( Class<? extends Easing> clazz, EaseType type, double fromValue, double endValue, int durationMillis, long delayMillis ) {
		if ( !mRunning ) {
			mEasing = createInstance( clazz );
			
			if( null == mEasing ){
				return;
			}
			
			mMethod = getEasingMethod( mEasing, type );
			if( mMethod == null ){
				return;
			}
			
			mInverted = fromValue > endValue;
			
			if( mInverted ){
				mStartValue = endValue;
				mEndValue = fromValue;
			} else {
				mStartValue = fromValue;
				mEndValue = endValue;
			}
			mValue = mStartValue;
			
			mDuration = durationMillis;
			mBase = SystemClock.uptimeMillis() + delayMillis;
			mRunning = true;
			mTicker = new Ticker();
			long next = SystemClock.uptimeMillis() + FRAME_TIME + delayMillis;
			
			if( delayMillis == 0 ) {
				mEasingCallback.onEasingStarted( fromValue );
			} else {
				mHandler.postAtTime( new TickerStart( fromValue ), mToken, next - FRAME_TIME );
			}
			
			mHandler.postAtTime( mTicker, mToken, next );
		}
	}
	
	/**
	 * Stop the current easing process. onEasingFinished will not be invoked
	 */
	public void stop(){
		mRunning = false;
		mHandler.removeCallbacks( mTicker, mToken );
	}
	
	Easing createInstance( Class<? extends Easing> clazz ){
		try {
			return clazz.newInstance();
		} catch ( IllegalAccessException e ) {
			e.printStackTrace();
		} catch ( InstantiationException e ) {
			e.printStackTrace();
		}
		return null;
	}
	
	Method getEasingMethod( Easing instance, EaseType type ){
		
		String methodName = getMethodName( type );
		if( null != methodName ){
			Method m;
			try {
				m = instance.getClass().getMethod( methodName, double.class, double.class, double.class, double.class );
			} catch ( SecurityException e ) {
				e.printStackTrace();
				return null;
			} catch ( NoSuchMethodException e ) {
				e.printStackTrace();
				return null;
			}
			return m;
		}
		return null;
	}
	
	String getMethodName( EaseType type ){
		switch( type ){
			case EaseIn:
				return "easeIn";
			case EaseInOut:
				return "easeInOut";
			case EaseNone:
				return "easeNone";
			case EaseOut:
				return "easeOut";
		}
		return null;
	}
	
	class TickerStart implements Runnable {
		
		double mValue;
		
		public TickerStart ( double value ) {
			mValue = value;
		}
	
		@Override
		public void run() {
			mEasingCallback.onEasingStarted( mValue );
		}
	};

	class Ticker implements Runnable {
		
		@Override
		public void run() {
			long base = mBase;
			long now = SystemClock.uptimeMillis();
			long diff = now - base;

			double old = mValue;
			double value;
			try {
				value = (Double) mMethod.invoke( mEasing, diff, mStartValue, mEndValue, mDuration );
			} catch ( IllegalArgumentException e ) {
				e.printStackTrace();
				return;
			} catch ( IllegalAccessException e ) {
				e.printStackTrace();
				return;
			} catch ( InvocationTargetException e ) {
				e.printStackTrace();
				return;
			}
			
			mValue = value;

			int frame = (int) ( diff / FRAME_TIME );
			long next = base + ( ( frame + 1 ) * FRAME_TIME );

			if ( diff < mDuration ) {
				mEasingCallback.onEasingValueChanged( mInverted ? mEndValue - value : value, old );
				mHandler.postAtTime( this, mToken, next );
			} else {
				mEasingCallback.onEasingFinished( mInverted ? mEndValue : mStartValue );
				mRunning = false;
			}
		}
	};

}
